package org.fatewa.engine.genius.base;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.IService;
import core.MultipartData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.fatewa.engine.advice.annotations.OriginalResponse;
import org.fatewa.engine.advice.exception.RayquazaException;
import org.fatewa.engine.design.base.PrimaryKeyProvider;
import org.fatewa.engine.design.genius.GenericHooks;
import org.fatewa.engine.experimental.poi.Intermediary;
import org.fatewa.engine.experimental.poi.WorkBookIntermediary;
import org.fatewa.engine.genius.utils.GenericUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static org.fatewa.engine.genius.utils.GenericUtils.transaction;


/**
 * 动态接口的模版实现
 *
 * @author 4everlynn
 */
@RestController
@RequestMapping
@Data
@Accessors(fluent = true, chain = true)
@Slf4j
public class GenericController<T, S extends IService<T>, P extends Serializable> {
    /**
     * 实体类型
     * 与 1.x 不同得是 rayquaza 不再提供 AncestorDomain 父类
     */
    private Class<T> entityClass;
    /**
     * 服务类型 {@link IService}
     */
    private Class<S> serviceClass;

    /**
     * 服务实例 {@link IService}
     */
    private IService<T> service;

    /**
     * Spring 上下文
     */
    private ApplicationContext context;

    /**
     * ID Key 映射的提器
     */
    private PrimaryKeyProvider<P> provider;

    /**
     * 当前类的 Hooks
     */
    private Collection<GenericHooks<T, ?>> hooks;

    public static final String BINARY_METHOD_NAME = "export";

    /**
     * 配置上下文时，顺便注入当前类的服务
     *
     * @param context Spring 上下文
     */
    public void context(ApplicationContext context) {
        this.context = context;
        this.service = context.getBean(serviceClass);
    }

    /**
     * 根据 ID 查询
     *
     * @param id 主键
     * @return 根据主键查询到的单个实体
     */
    @GetMapping("/{id}")
    public T single(@PathVariable P id) {

        // 使用 MultipartData 进行类型模糊转换
        MultipartData temp = new MultipartData().include("id", id);

        T instance = service
                .query()
                .eq(provider.key(), temp.getPart("id", provider.type(), true))
                .one();

        // spring boot 3.x 返回 null 时不会触发 advice，故需要自行处理一下
        if (null == instance) {
            throw new RayquazaException(HttpStatus.NOT_FOUND, "找不到对应数据");
        }

        return instance;
    }

    @GetMapping
    public IPage<T> query(HttpServletRequest request) {
        return GenericUtils.query(request, entityClass, service);
    }


    /**
     * 该接口需要对应实体类提供了 {@link Intermediary} 注解时才生效
     *
     * @param request 当前请求对象
     */
    @SneakyThrows
    @GetMapping("/workbooks")
    @OriginalResponse
    public void export(HttpServletRequest request, HttpServletResponse response) {

        Assert.notNull(entityClass, "Entity class cannot be null");
        // 获取当前实体类的媒介类型
        Intermediary intermediary = entityClass.getDeclaredAnnotation(Intermediary.class);
        Assert.notNull(intermediary, "Entity should having a execl description wrapper");

        String fileName = URLEncoder
                .encode(intermediary.filename(), "UTF-8")
                .replaceAll("\\+", "%20");

        // response 相关设置
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

        // 查询结果集
        IPage<T> page = this.query(request);

        List<WorkBookIntermediary<?>> webBookIntermediaries = new ArrayList<>();

        //noinspection unchecked
        Class<? extends WorkBookIntermediary<T>> intermediaryClass
                = (Class<? extends WorkBookIntermediary<T>>) intermediary.value();

        // 将查询结果映射为 Excel 所需的格式
        page.getRecords()
                .forEach(it -> {
                    WorkBookIntermediary<T> instance;
                    try {
                        instance = intermediaryClass.getDeclaredConstructor().newInstance();
                        webBookIntermediaries.add(instance.transfer(it, context));
                    } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                             NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                });

        // 流式写出
        EasyExcel.write(response.getOutputStream(), intermediaryClass)
                .sheet(intermediary.sheetName())
                .doWrite(webBookIntermediaries);
    }


    /**
     * 根据 ID 删除
     *
     * @param id 主键
     */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable P id) {
        // 使用 MultipartData 进行类型模糊转换
        MultipartData temp = new MultipartData().include("id", id);
        P primary = temp.getPart("id", provider.type(), true);
        QueryWrapper<T> wrapper = Wrappers.query();
        wrapper.eq(provider().key(), primary);
        service.remove(wrapper);
    }

    /**
     * 部分更新数据，如果没有传入主键，
     * 则会进行数据新增
     */
    @PatchMapping
    public T update(@RequestBody T target) {

        // Spring 处理 @RequestBody 时会将泛型改为 Map，这里引入 MultipartData 进行真实实体转换
        MultipartData multipartData = new MultipartData().parties(target);
        // 使用 MultipartData 进行类型模糊转换
        MultipartData temp = new MultipartData().include("id", multipartData.get(provider.key()));

        // 获取到用户传递的实体实例
        T instance = multipartData.get(entityClass, true);

        for (GenericHooks<T, ?> hook : hooks) {
            String name = hook.getClass().getSimpleName();
            Runnable beforeUpdate = () -> hook.beforeUpdate(instance, request());
            // 异步执行 hook
            if (hook.async()) {
                log.info("{} hook {}#beforeUpdate triggered in async state", entityClass.getSimpleName(), name);
                CompletableFuture.runAsync(beforeUpdate);
            } else {
                log.info("{} hook {}#beforeUpdate triggered", entityClass.getSimpleName(), name);
                beforeUpdate.run();
            }
        }

        P primary = temp.getPart("id", provider.type(), true);
        QueryWrapper<T> wrapper = Wrappers.query();
        wrapper.eq(provider().key(), primary);
        // 执行数据保存
        boolean success = service.update(instance, wrapper);

        for (GenericHooks<T, ?> hook : hooks) {

            String name = hook.getClass().getSimpleName();

            Runnable updated = () -> {
                // 开启 hook 并且开启事务
                if (hook.transactional()) {
                    transaction((manager, status)  -> hook.updated(instance, success), context);
                } else {
                    hook.updated(instance, success);
                }
            };


            // 异步执行 hook
            if (hook.async()) {
                log.info("{} hook {}#updated triggered in async state", entityClass.getSimpleName(), name);
                CompletableFuture.runAsync(updated);
            } else {
                log.info("{} hook {}#updated triggered", entityClass.getSimpleName(), name);
                updated.run();
            }
        }

        Assert.isTrue(success, "更新失败");

        return instance;
    }


    @PostMapping
    public T create(@RequestBody T target) {
        // Spring 处理 @RequestBody 时会将泛型改为 Map，这里引入 MultipartData 进行真实实体转换
        MultipartData multipartData = new MultipartData().parties(target);

        // 获取到用户传递的实体实例
        T instance = multipartData.get(entityClass, true);

        for (GenericHooks<T, ?> hook : hooks) {
            Runnable beforeCreate = () -> hook.beforeCreate(instance, request());

            String name = hook.getClass().getName();


            // 异步执行 hook
            if (hook.async()) {
                log.info("{} hook {}#beforeCreate triggered in async state", entityClass.getSimpleName(), name);
                CompletableFuture.runAsync(beforeCreate);
            } else {
                log.info("{} hook {}#beforeCreate triggered", entityClass.getSimpleName(), name);
                beforeCreate.run();
            }
        }
        // 执行数据保存
        final boolean success = service.save(instance);

        for (GenericHooks<T, ?> hook : hooks) {
            String name = hook.getClass().getSimpleName();
            Runnable created = () -> {
                // 开启 hook 并且开启事务
                if (hook.transactional()) {
                    transaction((manager, status)  -> hook.created(instance, success), context);
                } else {
                    hook.created(instance, success);
                }
            };

            // 异步执行 hook
            if (hook.async()) {
                log.info("{} hook {}#created triggered in async state", entityClass.getSimpleName(), name);
                CompletableFuture.runAsync(created);
            } else {
                log.info("{} hook {}#created triggered", entityClass.getSimpleName(), name);
                created.run();
            }
        }


        return instance;
    }

    /**
     * 获取当前的请求
     *
     * @return 当前的请求
     */
    private HttpServletRequest request() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (null == attributes) {
            return null;
        }
        RequestContextHolder.setRequestAttributes(attributes, true);
        return ((ServletRequestAttributes) attributes).getRequest();
    }

}
